/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements. See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.facebook.presto.bytecode.expression;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
import com.facebook.presto.bytecode.BytecodeNode;
import com.facebook.presto.bytecode.BytecodeVisitor;
import com.facebook.presto.bytecode.FieldDefinition;
import com.facebook.presto.bytecode.MethodDefinition;
import com.facebook.presto.bytecode.MethodGenerationContext;
import com.facebook.presto.bytecode.ParameterizedType;
import org.objectweb.asm.MethodVisitor;

import static com.facebook.presto.bytecode.BytecodeUtils.checkArgument;
import static com.facebook.presto.bytecode.ParameterizedType.type;
import static com.facebook.presto.bytecode.expression.BytecodeExpressions.constantInt;
import static java.util.Arrays.stream;
import static java.util.Objects.requireNonNull;

/**
 * A BytecodeExpression is chain of Java like expressions that results in at most
 * a single value being pushed on the stack.  The chain starts with a constant,
 * local variable, static field, static method or invoke dynamic followed
 * by zero or more invocations, field dereferences, array element fetches, or casts.
 * The expression can optionally be terminated by a set expression, and in this
 * case no value is pushed on the stack.
 * <p>
 * A BytecodeExpression is a BytecodeNode so it works with tools like tree dump.
 * <p>
 * This abstraction makes it easy to write generic byte code generators that can
 * work with data that may come from a parameter, field or the result of a method
 * invocation.
 */
public abstract class BytecodeExpression
    implements BytecodeNode {
    private final ParameterizedType type;

    protected BytecodeExpression(ParameterizedType type) {
        this.type = requireNonNull(type, "type is null");
    }

    public final ParameterizedType getType() {
        return type;
    }

    public abstract BytecodeNode getBytecode(MethodGenerationContext generationContext);

    protected abstract String formatOneLine();

    @Override
    public final String toString() {
        return formatOneLine() + (type.getPrimitiveType() == void.class ? ";" : "");
    }

    public final BytecodeExpression getField(Class<?> declaringClass, String name) {
        return new GetFieldBytecodeExpression(this, declaringClass, name);
    }

    public final BytecodeExpression getField(String name, Class<?> type) {
        return new GetFieldBytecodeExpression(this, this.getType(), name, type(type));
    }

    public final BytecodeExpression getField(Field field) {
        return new GetFieldBytecodeExpression(this, field);
    }

    public final BytecodeExpression getField(FieldDefinition field) {
        return new GetFieldBytecodeExpression(this, field);
    }

    public final BytecodeExpression getField(ParameterizedType declaringClass, String name, ParameterizedType type) {
        return new GetFieldBytecodeExpression(this, declaringClass, name, type);
    }

    public final BytecodeExpression setField(String name, BytecodeExpression value) {
        return new SetFieldBytecodeExpression(this, this.getType(), name, value);
    }

    public final BytecodeExpression setField(Field field, BytecodeExpression value) {
        return new SetFieldBytecodeExpression(this, field, value);
    }

    public final BytecodeExpression setField(FieldDefinition field, BytecodeExpression value) {
        return new SetFieldBytecodeExpression(this, field, value);
    }

    public final BytecodeExpression cast(Class<?> type) {
        return new CastBytecodeExpression(this, type(type));
    }

    public final BytecodeExpression cast(ParameterizedType type) {
        return new CastBytecodeExpression(this, type);
    }

    public final BytecodeExpression invoke(Method method, BytecodeExpression... parameters) {
        return invoke(method, List.of(requireNonNull(parameters, "parameters is null")));
    }

    public final BytecodeExpression invoke(MethodDefinition method,
        Collection<? extends BytecodeExpression> parameters) {
        List<BytecodeExpression> params = new ArrayList<>(parameters);

        checkArgument(method.getParameters().size() == params.size(), "Expected %s params found %s", method.getParameters().size(), params.size());
        return invoke(method.getName(), method.getReturnType(), method.getParameterTypes(), parameters);
    }

    public final BytecodeExpression invoke(Method method, Collection<? extends BytecodeExpression> parameters) {
        return invoke(
            method.getName(),
            type(method.getReturnType()),
            stream(method.getParameterTypes())
                .map(ParameterizedType::type)
                .collect(Collectors.toList()),
            parameters);
    }

    public final BytecodeExpression invoke(String methodName, Class<?> returnType, BytecodeExpression... parameters) {
        return invoke(methodName, type(returnType), List.of(requireNonNull(parameters, "parameters is null")));
    }

    public final BytecodeExpression invoke(String methodName, Class<?> returnType,
        Collection<? extends BytecodeExpression> parameters) {
        return invoke(methodName, type(returnType), parameters);
    }

    public final BytecodeExpression invoke(String methodName, ParameterizedType returnType,
        Collection<? extends BytecodeExpression> parameters) {
        requireNonNull(parameters, "parameters is null");

        return invoke(methodName,
            returnType,
            parameters.stream().map(BytecodeExpression::getType).collect(Collectors.toList()),
            parameters);
    }

    public final BytecodeExpression invoke(String methodName, Class<?> returnType,
        Collection<? extends Class<?>> parameterTypes, BytecodeExpression... parameters) {
        return invoke(methodName, type(returnType),
            parameterTypes.stream().map(ParameterizedType::type).collect(Collectors.toList()),
            List.of(requireNonNull(parameters, "parameters is null")));
    }

    public final BytecodeExpression invoke(String methodName, ParameterizedType returnType,
        Collection<ParameterizedType> parameterTypes, BytecodeExpression... parameters) {
        return invoke(methodName, returnType, parameterTypes, List.of(requireNonNull(parameters, "parameters is null")));
    }

    public final BytecodeExpression invoke(
        String methodName,
        ParameterizedType returnType,
        Collection<ParameterizedType> parameterTypes,
        Collection<? extends BytecodeExpression> parameters) {
        return InvokeBytecodeExpression.createInvoke(
            this,
            methodName,
            returnType,
            parameterTypes,
            parameters);
    }

    public final BytecodeExpression getElement(int index) {
        return new GetElementBytecodeExpression(this, constantInt(index));
    }

    public final BytecodeExpression getElement(BytecodeExpression index) {
        return new GetElementBytecodeExpression(this, index);
    }

    public final BytecodeExpression setElement(int index, BytecodeExpression value) {
        return new SetArrayElementBytecodeExpression(this, constantInt(index), value);
    }

    public final BytecodeExpression setElement(BytecodeExpression index, BytecodeExpression value) {
        return new SetArrayElementBytecodeExpression(this, index, value);
    }

    public final BytecodeExpression length() {
        return new ArrayLengthBytecodeExpression(this);
    }

    public final BytecodeExpression ret() {
        return new ReturnBytecodeExpression(this);
    }

    public final BytecodeExpression pop() {
        if (this.getType().getPrimitiveType() == void.class) {
            return this;
        }
        return new PopBytecodeExpression(this);
    }

    @Override
    public final void accept(MethodVisitor visitor, MethodGenerationContext generationContext) {
        getBytecode(generationContext).accept(visitor, generationContext);
    }

    @Override
    public final <T> T accept(BytecodeNode parent, BytecodeVisitor<T> visitor) {
        return visitor.visitBytecodeExpression(parent, this);
    }

    public BytecodeExpression instanceOf(Class<?> type) {
        return InstanceOfBytecodeExpression.instanceOf(this, type);
    }
}
